#ifndef MEMORYMANAGERCORE_H_INCLUDED
#define MEMORYMANAGERCORE_H_INCLUDED

#include "Constants.h"
#include "StackAllocator.h"
#include "HeapAllocator.h"
#include "PoolAllocator.h"
#include "Assertions.h"

#include <typeinfo>

namespace CEngine {

namespace MemoryManagerNothingToSeeHere {
    extern StackAllocator* tempStack;
    extern U8 currentTempStack;

    extern HeapAllocator heap;
    extern U64 heapMin;
    extern U64 heapMax;

    extern PoolAllocator pool24;
    extern U64 pool24Min;
    extern U64 pool24Max;

    extern StackAllocator stack;
    extern U64 stackMin;
    extern U64 stackMax;
    extern void stackAlloc(void* pointer, U64 bytes);
    extern void stackFree(void* pointer);

    extern bool initialized;

    extern void testMemory(void* pointer, U64 bytes, bool removeEntry = false);
}

namespace MemoryManager {
    template<class T>
    T* alloc(T object) {
        ASSERT(MemoryManagerNothingToSeeHere::initialized);

        U64 bytes = sizeof(T);
        T* pointer;
        if(bytes == 0) {
            return NULL;
        }
        else if(bytes == 24) {
            pointer = (T*)MemoryManagerNothingToSeeHere::pool24.alloc();

            static U8 errorLevel = 0;
            U64 elementsRemaining = MemoryManagerNothingToSeeHere::pool24.getFreeElements();
            if(elementsRemaining == 0) {
                if(errorLevel < 2) {
                    defaultLog.log(Logging::MEMORY, LOG_ERROR, "24 byte pool empty!\n");
                }
                errorLevel = 2;
            }
            else if(elementsRemaining < (Constants::Memory::pool24Memory * 0.05)) {
                if(errorLevel < 1) {
                    defaultLog.log(Logging::MEMORY, LOG_WARNING, "Remaining 24 byte pool elements below 5%%!\n");
                }
                errorLevel = 1;
            }
            else {
                errorLevel = 0;
            }
        }
        else if(bytes < Constants::Memory::heapPageSize && bytes <= MemoryManagerNothingToSeeHere::stack.getFreeBytes()) {
            pointer = (T*)MemoryManagerNothingToSeeHere::stack.alloc(bytes);
            MemoryManagerNothingToSeeHere::stackAlloc(pointer, bytes);

            static bool lowMemory = false;
            U64 bytesRemaining = MemoryManagerNothingToSeeHere::stack.getFreeBytes();
            if(bytesRemaining < (Constants::Memory::stackMemory * 0.05)) {
                if(!lowMemory) {
                    defaultLog.log(Logging::MEMORY, LOG_WARNING, "Remaining stack memory below 5%%!\nMay begin to use heap memory instead!\n");
                }
                lowMemory = true;
            }
            else {
                lowMemory = false;
            }
        }
        else {
            pointer = (T*)MemoryManagerNothingToSeeHere::heap.alloc(bytes);

            static U8 errorLevel = 0;
            U64 byteRemaining = MemoryManagerNothingToSeeHere::heap.getFreeBytes();
            if(byteRemaining == 0) {
                if(errorLevel < 2) {
                    defaultLog.log(Logging::MEMORY, LOG_ERROR, "heap empty!\n");
                }
                errorLevel = 2;
            }
            else if(byteRemaining < (Constants::Memory::heapMemory * 0.05)) {
                if(errorLevel < 1) {
                    defaultLog.log(Logging::MEMORY, LOG_WARNING, "Remaining heap memory below 5%%!\n");
                }
                errorLevel = 1;
            }
            else {
                errorLevel = 0;
            }
        }

        if(!pointer) {
            float outputBytes = bytes;
            if(outputBytes > (10 * 1024 * 1024)) {
                outputBytes = outputBytes / (1024 * 1024);
                defaultLog.log(Logging::MEMORY, LOG_ERROR, "Failed to allocate %.2fMB!\n", outputBytes);
            }
            else if(outputBytes > (10 * 1024)) {
                outputBytes = outputBytes / (1024);
                defaultLog.log(Logging::MEMORY, LOG_ERROR, "Failed to allocate %.2fkB!\n", outputBytes);
            }
            else {
                defaultLog.log(Logging::MEMORY, LOG_ERROR, "Failed to allocate %.0fB!\n", outputBytes);
            }
        }
        MemoryManagerNothingToSeeHere::testMemory(pointer, bytes);
        *pointer = object;
        return pointer;
    }

    template<class T>
    void free(T* pointer) {
        if(!pointer || !MemoryManagerNothingToSeeHere::initialized) {
            return;
        }

        pointer->~T();

        if((U64)pointer >= MemoryManagerNothingToSeeHere::heapMin && (U64)pointer <= MemoryManagerNothingToSeeHere::heapMax) {
            MemoryManagerNothingToSeeHere::heap.free(pointer);
        }
        else if((U64)pointer >= MemoryManagerNothingToSeeHere::stackMin && (U64)pointer <= MemoryManagerNothingToSeeHere::stackMax) {
            MemoryManagerNothingToSeeHere::stackFree(pointer);
        }
        else if((U64)pointer >= MemoryManagerNothingToSeeHere::pool24Min && (U64)pointer <= MemoryManagerNothingToSeeHere::pool24Max) {
            MemoryManagerNothingToSeeHere::pool24.free(pointer);
        }

        MemoryManagerNothingToSeeHere::testMemory(pointer, 0, true);

        return;
    }

    template<> inline
    void free(void* pointer) {
        if(!pointer || !MemoryManagerNothingToSeeHere::initialized) {
            return;
        }

        if((U64)pointer >= MemoryManagerNothingToSeeHere::heapMin && (U64)pointer <= MemoryManagerNothingToSeeHere::heapMax) {
            MemoryManagerNothingToSeeHere::heap.free(pointer);
        }
        else if((U64)pointer >= MemoryManagerNothingToSeeHere::stackMin && (U64)pointer <= MemoryManagerNothingToSeeHere::stackMax) {
            MemoryManagerNothingToSeeHere::stackFree(pointer);
        }
        else if((U64)pointer >= MemoryManagerNothingToSeeHere::pool24Min && (U64)pointer <= MemoryManagerNothingToSeeHere::pool24Max) {
            MemoryManagerNothingToSeeHere::pool24.free(pointer);
        }

        MemoryManagerNothingToSeeHere::testMemory(pointer, 0, true);

        return;
    }
}

}

#endif // MEMORYMANAGERCORE_H_INCLUDED
